Distance fog: height falloff, so mist pools in low ground - #1634
Merged
Conversation
Uniform fog gives one dial for two jobs. Tune it so a valley has
atmosphere and the skyline washes out with it; tune it so the peaks stay
crisp and the low ground has no air in it. The fog cannot tell which is
high and which is low, because density is the same everywhere and only
the distance through it varies.
`heightFalloff` adds the second falloff. Density drops exponentially with
altitude, so mist fills the hollows and thins over the ridges.
It multiplies the DISTANCE rather than adding a third curve, so "linear"
and "exp2" are untouched and both gain it for free. An exponential
integrates analytically along a straight segment, so the whole ray costs
one `exp` in the vertex stage — no marching, no volume texture.
`heightFalloff` defaults to 0, and that is not a special case: `kdy` is
zero, the series limit is taken, `exp(0)` is one, and the factor is
exactly 1. A scene that omits it renders identically, which the first
test asserts byte for byte.
Three things the issue predicted would bite, all handled and two of them
mutation-checked:
- Y-DOWN. Every published form assumes Y-up, so density rises as `y`
INCREASES here. Flipping it puts the mist on the peaks instead of in
the valley, and the test catches it.
- The horizontal ray. `(exp(x) - 1) / x` is 0/0 when the view ray is
level, which is the common case looking across a valley. The series
limit is taken rather than a guard, or the fog would step as the ray
flattened; removing it fails seven tests.
- Overflow with the camera far below the reference height. Clamped.
Noted honestly in the test: this one is NOT observable through the
fog curve, which clamps an infinite distance anyway. It is insurance
against a driver carrying `Inf` into an interpolated varying.
The WebGPU uniform block grows 240 -> 256; the three specs that pin its
size are re-pinned deliberately.
Verified on screen on both backends: mist filling the run while the
wooded walls stay crisp, and the unfogged examples unchanged.
Closes #1633
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Aa37KGXZcnVrbn1yG4j1N
Contributor
There was a problem hiding this comment.
Pull request overview
This PR extends melonJS’s existing 3D distance fog to support height-based density falloff, enabling “mist pooling” in low ground while keeping higher terrain/crisp skylines less fogged. It does this by multiplying the existing fog distance term by an analytically integrated height factor, keeping the existing "linear" and "exp2" fog curves intact.
Changes:
- Adds
fogHeightandheightFalloffoptions/state toCamera3d.setFog()and the fog state passed to renderers/shaders. - Extends WebGL + WebGPU mesh vertex shaders to scale
vFogDepthby a height-integral factor (fragment fog code stays unchanged). - Grows the WebGPU
MeshUniformsblock from 240 → 256 bytes to carry the additional fog-height parameters, and updates/expands tests accordingly.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/melonjs/tests/webgpu_mtl_material.spec.js | Updates WebGPU mesh uniform block size expectation to 256 bytes. |
| packages/melonjs/tests/webgpu_mesh_fog.spec.js | Updates WebGPU fog uniform block sizing/layout-key tests; adds new height-falloff-related defaults in fixtures. |
| packages/melonjs/tests/webgpu_mesh_batcher.spec.js | Updates WebGPU pipeline layout key and uniform size expectations to mesh:u256 / 256 bytes. |
| packages/melonjs/tests/webgl_mesh_fog.spec.js | Adds WebGL integration tests covering zero-falloff equivalence, Y-down sign, horizontal-ray degeneracy, and extreme camera height behavior. |
| packages/melonjs/src/video/webgpu/shaders/mesh.wgsl | Adds fogHeight uniform vec4 and applies height factor to fog depth in unlit mesh WGSL. |
| packages/melonjs/src/video/webgpu/shaders/mesh-shadow-instanced.wgsl | Applies the same height-based fog depth scaling to instanced shadow blobs for backend consistency. |
| packages/melonjs/src/video/webgpu/shaders/mesh-lit.wgsl | Adds fogHeight uniform vec4 and applies height factor to fog depth in lit mesh WGSL. |
| packages/melonjs/src/video/webgpu/shaders/mesh-instanced.js | Updates generated instanced WGSL vertex bodies to include height factor when writing fog depth. |
| packages/melonjs/src/video/webgpu/batchers/mesh_batcher.js | Expands the per-draw uniform packing to 256 bytes and writes the new fog-height parameters at floats 60–63. |
| packages/melonjs/src/video/webgl/shaders/mesh.vert | Adds uFogHeight and scales fog depth by fogHeightFactor in the unlit WebGL vertex shader. |
| packages/melonjs/src/video/webgl/shaders/mesh-shadow-instanced.vert | Adds uFogHeight and scales fog depth similarly for instanced shadow rendering. |
| packages/melonjs/src/video/webgl/shaders/mesh-lit.vert | Adds uFogHeight and scales fog depth for the lit WebGL vertex shader. |
| packages/melonjs/src/video/webgl/shaders/mesh-lit-instanced.vert | Adds uFogHeight and scales fog depth for lit instanced meshes. |
| packages/melonjs/src/video/webgl/shaders/mesh-instanced.vert | Adds uFogHeight and scales fog depth for unlit instanced meshes. |
| packages/melonjs/src/video/webgl/batchers/mesh_batcher.js | Adds uFogHeight uniform upload/caching for WebGL mesh draws. |
| packages/melonjs/src/camera/fog.ts | Extends fog option/state types with fogHeight, heightFalloff, and cameraY. |
| packages/melonjs/src/camera/camera3d.ts | Validates/stores new fog options and resolves per-frame fog state including camera Y for shader integration. |
| packages/melonjs/skills/melonjs-3d/SKILL.md | Updates 3D skill docs/examples to include height falloff usage and Y-down sign caveats. |
| packages/melonjs/CHANGELOG.md | Adds release note entry describing the new height falloff fog feature and its behavior. |
Suppressed comments (1)
packages/melonjs/tests/webgpu_mesh_fog.spec.js:105
- The uniform-layout test verifies fogColor (52-54) and fogParams (56-59), but doesn't assert the new fogHeight vec4 (60-63). Adding an assertion here would pin the new offsets and ensure the new fields are zeroed/written correctly.
it("writes the fog colour at 52-54 and the params at 56-59", () => {
install(fog());
batcher.drawRetainedMesh(makeMesh(), MODEL, 0xffffffff);
const floats = snapshot();
// model 0-15, view 16-31, tint 32-35, params 36-39, emissive 40-43,
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+39
to
+42
| // Density falls off exponentially with altitude, and an exponential integrates | ||
| // analytically along a straight segment, so the whole ray costs one `exp` and | ||
| // no marching. The result multiplies the distance, which leaves both fog | ||
| // curves exactly as they are. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1633. Builds on #1622.
The problem
Uniform fog is one dial doing two jobs. Density is the same everywhere, so only the distance through it varies — a ridge top 3000 units away fogs exactly as much as the valley floor 3000 units away. Tune it so a valley has atmosphere and the skyline washes out with it; tune it so the peaks stay crisp and the low ground has no air in it at all.
The change
Density drops exponentially with altitude, so mist fills the hollows and thins over the ridges. That is the difference between fog reading as weather and reading as a global desaturation.
It multiplies the distance rather than adding a third curve, so
"linear"and"exp2"are untouched and both gain it for free. An exponential integrates analytically along a straight segment, so the whole ray costs oneexpin the vertex stage — no marching, no volume texture.Additive, and provably so
heightFalloffdefaults to0, and that is not a special case:kdyis zero, the series limit is taken,exp(0)is one, and the factor is exactly1. The first test asserts a zero-falloff frame is byte for byte the frame without the feature.The three traps the issue predicted
yincreases here. Flipping it puts mist on the peaks instead of in the valley — mutation-checked, one test fails(exp(x) − 1) / xis0/0when the ray is level, which is the common case looking across a valley. The series limit is taken rather than a guard, or fog would step as the ray flattened — mutation-checked, seven tests failInfinto an interpolated varying, which this cannot observeNotes
Verification
270 files / 6531 tests, lint and types clean. Checked on screen on both backends — WebGL, and WebGPU on a real Metal adapter: mist filling the valley run while the wooded walls stay crisp, and the unfogged examples (Instanced Forest, glTF, AfterBurner) unchanged.🤖 Generated with Claude Code
https://claude.ai/code/session_012Aa37KGXZcnVrbn1yG4j1N